In this project, we are going to develop very basic methodologies for sentiment analysis including Logistic Regression, Naive Bayes, and Vector Space Classification and test their performance on a dataset I have found online. There are multiple R packages that are needed for this project. You can use the code install.packages(“Package Name”) to install the packages listed below.

library(rwhatsapp)
library(PersianStemmer)
library(plotly)
library(ggplot2)
library(wordcloud2)

Loading & Preparing the data

loading the DigiKala data related to iPhone that I have found online.

dat = read.csv("Data.txt", header = T, encoding = "UTF-8")
#at=read_excel("Data.xlsx")

dat=na.omit(dat)

By considering the label “1” as “Positive” and the rest of the labels as “Negative”, we are preparing the data for creating a model for detecting the sentiments from a comment.

Corpus=lookup_emoji(dat$X.U.FEFF.Text, text_field = "text")

Text = Corpus$text
Emoji = Corpus$emoji

Y=as.numeric(dat$Suggestion==1)

Defining functions for text cleaning, building up a dictionary, and extracitng features. First, the function that prepares the text for the analysis.

RefineText<- function(tex){
  n=length(tex)
  TextAd=c()
  
  #progress bar
  pb = txtProgressBar(min = 0, max = n, initial = 0,
                      style = 3, width = 50, char = "=") 
  
  for (i in 1:n) {
    tex[i] = RefineChars(tex[i]) 
    tex[i] = gsub("[\r\n]", "", tex[i])
    tex[i] = gsub('[[:punct:] ]+',' ', tex[i])
    tex[i] = RefineChars(tex[i])
        
    #,"@","$","%","&","*","،",".....","..."
    if (!(is.na(tex[i]))){
      if (all(tex[i]!=c(""," ","،","،,","‌"))){
        TextAd[i] = PerStem(tex[i], NoEnglish = F, NoNumbers = F,
                        NoStopwords = T, NoPunctuation = T,
                        StemVerbs = T, NoPreSuffix = T,
                    Context = T, StemBrokenPlurals = T,
                    Transliteration = F)
      }
    }
    setTxtProgressBar(pb,i)
  }
return(TextAd)
}

Now, it is time to build up a dictionary.

BuildFreqs <- function(corpus,y) {
  n = length(y)
d=1 # dictionary word numerator
freqs=matrix(c(0,0),ncol = 2) #frequencies for each word
colnames(freqs)=c("Neg","Pos")
Dict=c() 
t=c()
e=c()

tex = corpus$text
emoji = corpus$emoji
#refining Text removing numbers, stop words, punctuation, and so

#Progress bar
pb = txtProgressBar(min = 0, max = n, initial = 0, style = 3, width = 50, char = "=")
for (i in 1:n) {
  t = sort(table(unlist(strsplit(tex[i], " "))), decreasing = TRUE)
  
  #Building freqs for words
  for (j in 1:length(t)) {
    cond = (Dict == names(t)[j])
    if  (any(cond)) {
      wh = which(cond)
      freqs[wh+1,(y[i]+1)] = freqs[wh+1,(y[i]+1)]+t(j) # Adding Frequency
    } else if(is.null(names(t))==F) {
      Dict[d] = names(t)[j]
      if (y[i]==1) {
        freqs = rbind(freqs,c(0,1)) # Defining Frequency
      } else {
        freqs = rbind(freqs,c(1,0)) # Defining Frequency 
      }
      d = d+1
    }
  }
  
  if(is.null(emoji[[i]])==F){
    e = table(emoji[i])
    
    #Building frequencies for emojis
    for (j in 1:length(e)) {
      cond = (Dict == names(e)[j])
      if  (any(cond)) {
        wh = which(cond)
        freqs[wh+1,(y[i]+1)] = freqs[wh+1,(y[i]+1)]+e[j] 
        # Adding Frequency
      } else if(is.null(names(e))==F) {
        Dict[d] = names(e)[j]
        if (y[i]==1) {
          freqs = rbind(freqs,c(0,1)) # Defining Frequency
        } else {
          freqs = rbind(freqs,c(1,0)) # Defining Frequency 
        }
        
        d = d+1
      }
    }
  }
  
  setTxtProgressBar(pb,i)
}
Dictionary=list("Words"=Dict,"Frequencies"=freqs[-1,])
  return(Dictionary)
}

Finally, a function that uses the dictionary to extract features.

ExtractFeatures <- function(corpus, dict){
  
  tex = corpus$text
  emoji = corpus$emoji
  
  Words=dict$Words
  dict$Frequencies = dict$Frequencies/rowSums(dict$Frequencies)
  Frequencies=dict$Frequencies
  
  #Progress bar
  n = length(tex)
  pb = txtProgressBar(min = 0, max = n, initial = 0, style = 3,
width = 50, char = "=")
  
   x = matrix(rep(0,n*3),nrow =n ,ncol=3)
  for (i in 1:n) {
    wordlist = names(sort(table(unlist(strsplit(tex[i], " "))), 
                          decreasing = TRUE))
    if(is.null(emoji[[i]])==F){append(wordlist,table(emoji[i]))}
    x[i,1] = 1 #bias term is set to 1
    # loop through each word in the list of words
    for (word in wordlist){ 
      if (any(Words==word)) {
        # increment the word count for the neutral label 0
        x[i,2] = x[i,2]+Frequencies[which(Words==word),1]
       # increment the word count for the positive label 1
        x[i,3] = x[i,3]+Frequencies[which(Words==word),2]
      }
    }
    setTxtProgressBar(pb,i)
  }
 return(x) 
}

Using the functions defined, we are going to clean our Persian text, then build a dictionary, and extract features from the text.

TextAd=RefineText(Text)
Dictionary=BuildFreqs(list("text"=TextAd,"emoji"=Emoji),Y)
X = ExtractFeatures(list("text"=TextAd,"emoji"=Emoji), Dictionary)

Checking the features interpretability

To see, weather it is possible to to classify our text using the features we built, We must check the below scatter plot.

mylabel <- c("Train Pos", "Train Neg")
colors <- c("blue", "red")

xlabel = "Sum of Negative Words"
ylabel = "Sum of Positive Words"

plot_ly(x=X[,2],y=X[,3],
        symbols = c('x','o'),
        marker = list(size = 10),
        text= c(1:length(Y)), hoverinfo = "text",
        type="scatter",mode="markers", color=as.factor(Y),colors = c("red",  "green")) %>%
        layout(title = "Comments by the Features built",
               autosize = F, width = 920, height = 500)

The first classification method that is widely being used as a trivial and easy-to-implement methodology is Naive Bayes. In the following lines of code, we are going to build two functions by which we can use this method for our data.

ExtractSense=function(corpus, dict){
  
  tex = corpus$text
  emoji = corpus$emoji
  
  Words=dict$Words
  Frequencies=dict$Frequencies
  
  d=length(dict$Words)
  n=length(tex)
  
  #Laplacian Smoothing
  Freqs=1/(colSums(Frequencies)+d)*(t(Frequencies)+1)
  Freqs=t(Freqs)
  
  p_w_pos=Freqs[,2]
  p_w_neg=Freqs[,1]
  
  loglikelihood = log(p_w_pos/p_w_neg)
  
  p=rep(0,n)
  
  pb = txtProgressBar(min = 0, max = n, initial = 0, style = 3,
width = 50, char = "=")
  
  for(i in 1:n){
    
    wordlist = sort(table(unlist(strsplit(tex[i], " "))), decreasing = TRUE)
    if(is.null(emoji[[i]])==F){append(wordlist,table(emoji[i]))}
    
    for (j in 1:length(wordlist)){
      if  (any(Words == names(wordlist)[j])) {
        p[i]=p[i]+wordlist[j]*loglikelihood[Words == names(wordlist)[j]]
      }
    }
    setTxtProgressBar(pb,i)
  }
  return(p)
}

And, the predictior function will be defined like this:

Now, we want to use this method on our data to build a feature called “Sense” that measures the intensity of each comment alongside their polarity.

Sense=ExtractSense(list("text"=TextAd,"emoji"=Emoji), Dictionary)

We can see below how this feature looks like.

plot_ly(x=1:length(Sense),y=Sense, type = 'bar') %>%
  layout(title = "Overal Polarity of All the Comments",
         plot_bgcolor='#e5ecf6',autosize = T, width = 880, height = 500)

The accuracy of Naive Bayes classifier on the train dataset can be calculated as follow.


NB=NaiveBayesPredictor(Sense,Y)
yhat=NB$Yhat


NBAccuracy=c()
NBAccuracy["Positive"] = 
  mean(na.omit(Y[Y==1]==yhat[Y==1]))
NBAccuracy["Negative"] = 
  mean(na.omit(Y[Y==0]==yhat[Y==0]))

cat(paste0("Overal Accuracy: \n"),mean(yhat==Y),
    paste0("\nAccuracy for different sentiments: \n"), names(NBAccuracy),
    paste0(" \n"),NBAccuracy)
Overal Accuracy: 
 0.8727384 
Accuracy for different sentiments: 
 Positive Negative  
 0.9265323 0.7269625

The second classifier is based on vector space models. We are going to build such a classifier in the following lines of code.

VectorSpaceModel= function(x,y){
  
  pos_center=colMeans(x[y==1,])
  neg_center=colMeans(x[y==0,])
  centers=cbind(pos_center[-1],neg_center[-1])

  dot_prods=x[,-1]%*%centers
  normX=apply(x[,-1],1,function(x)(sqrt(sum(x^2))))
  normCenter=apply(centers,2,function(x)(sqrt(sum(x^2))))
  
  angel_cosine=as.matrix(1/normX)%*%t(as.matrix(1/normCenter))*(dot_prods)
  
  yhat=apply(angel_cosine, 1, function(x) which(max(x)==x))
  yhat[yhat==2]=0
  return(list("Yhat"=yhat,"Centers"=centers))
}

The next function is going to calculates (predicts) the polarity for each comment. Plus, if we input the dependent variable (y) as well it gives us the accuracy of the predictions.

VectorSpacePredictor= function(x,y=NA,centers){

  dot_prods=x[,-1]%*%centers
  normX=apply(x[,-1],1,function(x)(sqrt(sum(x^2))))
  normCenter=apply(centers,2,function(x)(sqrt(sum(x^2))))
  
  angel_cosine=as.matrix(1/normX)%*%t(as.matrix(1/normCenter))*(dot_prods)
  
  yhat=apply(angel_cosine, 1, function(x) which(max(x)==x))
  yhat[yhat==2]=0
  
  accuracy= NA
  
  if(is.na(y)==FALSE && length(y)==length(yhat)){
    accuracy = mean(y==yhat)
  }
  return(list("Yhat"=yhat,"Accuracy"=accuracy))
}

Train accuracy for Vector Space classifier is as follows.


fitVS=VectorSpaceModel(X,Y)
yhat=fitVS$Yhat


VSAccuracy=c()
VSAccuracy["Positive"] = 
  mean(na.omit(Y[Y==1]==yhat[Y==1]))
VSAccuracy["Negative"] = 
  mean(na.omit(Y[Y==0]==yhat[Y==0]))

cat(paste0("Overal Accuracy: \n"),mean(yhat==Y),
    paste0("\nAccuracy for different sentiments: \n"), names(VSAccuracy),
    paste0(" \n"),VSAccuracy)
Overal Accuracy: 
 0.9043238 
Accuracy for different sentiments: 
 Positive Negative  
 0.9609572 0.7508532

The logistic regression classifier accuracy on this dataset can be calculated as follow.

fitGLM=glm(Y~X[,-1],family="binomial")
yhat=round(predict(fitGLM,as.data.frame(X),type="response"),0)

GLMAccuracy=c()
GLMAccuracy["Positive"] = 
  mean(na.omit(Y[Y==1]==yhat[Y==1]))
GLMAccuracy["Negative"] = 
  mean(na.omit(Y[Y==0]==yhat[Y==0]))

cat(paste0("Overal Accuracy: \n"),mean(yhat==Y),
    paste0("\nAccuracy for different sentiments: \n"), names(GLMAccuracy),
    paste0(" \n"),GLMAccuracy)
Overal Accuracy: 
 0.9098436 
Accuracy for different sentiments: 
 Positive Negative  
 0.9718724 0.741752

It is not obvious which method performs better on this dataset. So, we run a simulation of 100 iterations each time we are going to randomly split the dataset into train and test. Then we build our models based on the train sets and test their performances on the test sets. the results we be stored in a matrix and after the simulation will be visualized.

So, the simulation for the defined methodologies is going to be like this.

So, in the following lines of code, we are going to build a box plot to compare the error of each method.

fig <- plot_ly(type = 'box')
fig <- fig %>% add_boxplot(y = Error[,1], jitter = 0.3, pointpos = -1.8, boxpoints = 'all',
              marker = list(color = 'rgb(7,40,89)'),
              line = list(color = 'rgb(7,40,89)'),
              name = "Vector Spcae Classifier")
fig <- fig %>% add_boxplot(y = Error[,2], jitter = 0.3, pointpos = -1.8, boxpoints = 'all',
              marker = list(color = 'rgb(9,56,125)'),
              line = list(color = 'rgb(9,56,125)'),
              name = "Naive Bayes Classifier")
fig <- fig %>% add_boxplot(y = Error[,3], jitter = 0.3, pointpos = -1.8, boxpoints = 'all',
              marker = list(color = 'rgb(107,174,214)'),
              line = list(color = 'rgb(107,174,214)'),
              name = "Logistic Regression Classifier")

fig %>% layout(autosize = F, width = 920, height = 500)

It seems obvious now that vector space classifier performs slightly better than naive Bayes and significantly better than Logistic regression classifier.

Visualizations helpful for a sentiment analysis report.

The doughnut chart for sentiment breakdown of the corpus.

Values <- c(length(Y[Y==1]),length(Y[Y==0]))
labels <- c("Positive", "Nagative" )
colors <- c("green", "red")

fig1 <- plot_ly(as.data.frame(Values), labels = labels, values = Values) %>%
  add_pie(hole = 0.6, marker=list(colors=colors)) %>%
  layout(title = "Sentiment Breakdown",  showlegend = F, 
         xaxis = list(showgrid = FALSE, zeroline = FALSE, showticklabels = TRUE),
         yaxis = list(showgrid = FALSE, zeroline = FALSE, showticklabels = TRUE))
fig1 %>% layout(autosize = F, width = 800, height = 500)

The Gauge chart for overall sentiment score of the corpus.

Finally, a wordcloud can beautify your report.

LS0tDQp0aXRsZTogIlNlbnRpbWVudCBBbmFseXNpcyBmb3IgUGVyc2lhbiBUZXh0Ig0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KDQpJbiB0aGlzIHByb2plY3QsIHdlIGFyZSBnb2luZyB0byBkZXZlbG9wIHZlcnkgYmFzaWMgbWV0aG9kb2xvZ2llcyBmb3Igc2VudGltZW50IGFuYWx5c2lzIGluY2x1ZGluZyAqKkxvZ2lzdGljIFJlZ3Jlc3Npb24qKiwNCioqTmFpdmUgQmF5ZXMqKiwgYW5kICoqVmVjdG9yIFNwYWNlIENsYXNzaWZpY2F0aW9uKiogYW5kIHRlc3QgdGhlaXIgcGVyZm9ybWFuY2Ugb24gYSBkYXRhc2V0IEkgaGF2ZSBmb3VuZCBvbmxpbmUuIFRoZXJlIGFyZSBtdWx0aXBsZSBSIHBhY2thZ2VzIHRoYXQgYXJlIG5lZWRlZCBmb3IgdGhpcyBwcm9qZWN0LiBZb3UgY2FuIHVzZSB0aGUgY29kZSAqKmluc3RhbGwucGFja2FnZXMoIlBhY2thZ2UgTmFtZSIpKiogdG8gaW5zdGFsbCB0aGUgcGFja2FnZXMgbGlzdGVkIGJlbG93Lg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShyd2hhdHNhcHApDQpsaWJyYXJ5KFBlcnNpYW5TdGVtbWVyKQ0KbGlicmFyeShwbG90bHkpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KHdvcmRjbG91ZDIpDQpgYGANCg0KDQojIExvYWRpbmcgJiBQcmVwYXJpbmcgdGhlIGRhdGENCg0KbG9hZGluZyB0aGUgRGlnaUthbGEgZGF0YSByZWxhdGVkIHRvIGlQaG9uZSB0aGF0IEkgaGF2ZSBmb3VuZCBvbmxpbmUuDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KZGF0ID0gcmVhZC5jc3YoIkRhdGEudHh0IiwgaGVhZGVyID0gVCwgZW5jb2RpbmcgPSAiVVRGLTgiKQ0KI2F0PXJlYWRfZXhjZWwoIkRhdGEueGxzeCIpDQoNCmRhdD1uYS5vbWl0KGRhdCkNCg0KYGBgDQoNCg0KQnkgY29uc2lkZXJpbmcgdGhlIGxhYmVsICIxIiBhcyAiUG9zaXRpdmUiIGFuZCB0aGUgcmVzdCBvZiB0aGUgbGFiZWxzIGFzICJOZWdhdGl2ZSIsIHdlIGFyZSBwcmVwYXJpbmcgdGhlIGRhdGEgZm9yIGNyZWF0aW5nIGEgbW9kZWwgZm9yIGRldGVjdGluZyB0aGUgc2VudGltZW50cyBmcm9tIGEgY29tbWVudC4NCmBgYHtyfQ0KQ29ycHVzPWxvb2t1cF9lbW9qaShkYXQkWC5VLkZFRkYuVGV4dCwgdGV4dF9maWVsZCA9ICJ0ZXh0IikNCg0KVGV4dCA9IENvcnB1cyR0ZXh0DQpFbW9qaSA9IENvcnB1cyRlbW9qaQ0KDQpZPWFzLm51bWVyaWMoZGF0JFN1Z2dlc3Rpb249PTEpDQpgYGANCg0KDQpEZWZpbmluZyBmdW5jdGlvbnMgZm9yIHRleHQgY2xlYW5pbmcsIGJ1aWxkaW5nIHVwIGEgZGljdGlvbmFyeSwgYW5kIGV4dHJhY2l0bmcgZmVhdHVyZXMuIEZpcnN0LCB0aGUgZnVuY3Rpb24gdGhhdCBwcmVwYXJlcyB0aGUgdGV4dCBmb3IgdGhlIGFuYWx5c2lzLg0KYGBge3J9DQpSZWZpbmVUZXh0PC0gZnVuY3Rpb24odGV4KXsNCiAgbj1sZW5ndGgodGV4KQ0KICBUZXh0QWQ9YygpDQogIA0KICAjcHJvZ3Jlc3MgYmFyDQogIHBiID0gdHh0UHJvZ3Jlc3NCYXIobWluID0gMCwgbWF4ID0gbiwgaW5pdGlhbCA9IDAsDQogICAgICAgICAgICAgICAgICAgICAgc3R5bGUgPSAzLCB3aWR0aCA9IDUwLCBjaGFyID0gIj0iKSANCiAgDQogIGZvciAoaSBpbiAxOm4pIHsNCiAgICB0ZXhbaV0gPSBSZWZpbmVDaGFycyh0ZXhbaV0pIA0KICAgIHRleFtpXSA9IGdzdWIoIltcclxuXSIsICIiLCB0ZXhbaV0pDQogICAgdGV4W2ldID0gZ3N1YignW1s6cHVuY3Q6XSBdKycsJyAnLCB0ZXhbaV0pDQogICAgdGV4W2ldID0gUmVmaW5lQ2hhcnModGV4W2ldKQ0KICAgICAgICANCiAgICAjLCJAIiwiJCIsIiUiLCImIiwiKiIsItiMIiwiLi4uLi4iLCIuLi4iDQogICAgaWYgKCEoaXMubmEodGV4W2ldKSkpew0KICAgICAgaWYgKGFsbCh0ZXhbaV0hPWMoIiIsIiAiLCLYjCIsItiMLCIsIuKAjCIpKSl7DQogICAgICAgIFRleHRBZFtpXSA9IFBlclN0ZW0odGV4W2ldLCBOb0VuZ2xpc2ggPSBGLCBOb051bWJlcnMgPSBGLA0KICAgICAgICAgICAgICAgICAgICAgICAgTm9TdG9wd29yZHMgPSBULCBOb1B1bmN0dWF0aW9uID0gVCwNCiAgICAgICAgICAgICAgICAgICAgICAgIFN0ZW1WZXJicyA9IFQsIE5vUHJlU3VmZml4ID0gVCwNCiAgICAgICAgICAgICAgICAgICAgQ29udGV4dCA9IFQsIFN0ZW1Ccm9rZW5QbHVyYWxzID0gVCwNCiAgICAgICAgICAgICAgICAgICAgVHJhbnNsaXRlcmF0aW9uID0gRikNCiAgICAgIH0NCiAgICB9DQogICAgc2V0VHh0UHJvZ3Jlc3NCYXIocGIsaSkNCiAgfQ0KcmV0dXJuKFRleHRBZCkNCn0NCmBgYA0KDQoNCk5vdywgaXQgaXMgdGltZSB0byBidWlsZCB1cCBhIGRpY3Rpb25hcnkuDQpgYGB7cn0NCkJ1aWxkRnJlcXMgPC0gZnVuY3Rpb24oY29ycHVzLHkpIHsNCiAgbiA9IGxlbmd0aCh5KQ0KZD0xICMgZGljdGlvbmFyeSB3b3JkIG51bWVyYXRvcg0KZnJlcXM9bWF0cml4KGMoMCwwKSxuY29sID0gMikgI2ZyZXF1ZW5jaWVzIGZvciBlYWNoIHdvcmQNCmNvbG5hbWVzKGZyZXFzKT1jKCJOZWciLCJQb3MiKQ0KRGljdD1jKCkgDQp0PWMoKQ0KZT1jKCkNCg0KdGV4ID0gY29ycHVzJHRleHQNCmVtb2ppID0gY29ycHVzJGVtb2ppDQojcmVmaW5pbmcgVGV4dCByZW1vdmluZyBudW1iZXJzLCBzdG9wIHdvcmRzLCBwdW5jdHVhdGlvbiwgYW5kIHNvDQoNCiNQcm9ncmVzcyBiYXINCnBiID0gdHh0UHJvZ3Jlc3NCYXIobWluID0gMCwgbWF4ID0gbiwgaW5pdGlhbCA9IDAsIHN0eWxlID0gMywgd2lkdGggPSA1MCwgY2hhciA9ICI9IikNCmZvciAoaSBpbiAxOm4pIHsNCiAgdCA9IHNvcnQodGFibGUodW5saXN0KHN0cnNwbGl0KHRleFtpXSwgIiAiKSkpLCBkZWNyZWFzaW5nID0gVFJVRSkNCiAgDQogICNCdWlsZGluZyBmcmVxcyBmb3Igd29yZHMNCiAgZm9yIChqIGluIDE6bGVuZ3RoKHQpKSB7DQogICAgY29uZCA9IChEaWN0ID09IG5hbWVzKHQpW2pdKQ0KICAgIGlmICAoYW55KGNvbmQpKSB7DQogICAgICB3aCA9IHdoaWNoKGNvbmQpDQogICAgICBmcmVxc1t3aCsxLCh5W2ldKzEpXSA9IGZyZXFzW3doKzEsKHlbaV0rMSldK3QoaikgIyBBZGRpbmcgRnJlcXVlbmN5DQogICAgfSBlbHNlIGlmKGlzLm51bGwobmFtZXModCkpPT1GKSB7DQogICAgICBEaWN0W2RdID0gbmFtZXModClbal0NCiAgICAgIGlmICh5W2ldPT0xKSB7DQogICAgICAgIGZyZXFzID0gcmJpbmQoZnJlcXMsYygwLDEpKSAjIERlZmluaW5nIEZyZXF1ZW5jeQ0KICAgICAgfSBlbHNlIHsNCiAgICAgICAgZnJlcXMgPSByYmluZChmcmVxcyxjKDEsMCkpICMgRGVmaW5pbmcgRnJlcXVlbmN5IA0KICAgICAgfQ0KICAgICAgZCA9IGQrMQ0KICAgIH0NCiAgfQ0KICANCiAgaWYoaXMubnVsbChlbW9qaVtbaV1dKT09Ril7DQogICAgZSA9IHRhYmxlKGVtb2ppW2ldKQ0KICAgIA0KICAgICNCdWlsZGluZyBmcmVxdWVuY2llcyBmb3IgZW1vamlzDQogICAgZm9yIChqIGluIDE6bGVuZ3RoKGUpKSB7DQogICAgICBjb25kID0gKERpY3QgPT0gbmFtZXMoZSlbal0pDQogICAgICBpZiAgKGFueShjb25kKSkgew0KICAgICAgICB3aCA9IHdoaWNoKGNvbmQpDQogICAgICAgIGZyZXFzW3doKzEsKHlbaV0rMSldID0gZnJlcXNbd2grMSwoeVtpXSsxKV0rZVtqXSANCiAgICAgICAgIyBBZGRpbmcgRnJlcXVlbmN5DQogICAgICB9IGVsc2UgaWYoaXMubnVsbChuYW1lcyhlKSk9PUYpIHsNCiAgICAgICAgRGljdFtkXSA9IG5hbWVzKGUpW2pdDQogICAgICAgIGlmICh5W2ldPT0xKSB7DQogICAgICAgICAgZnJlcXMgPSByYmluZChmcmVxcyxjKDAsMSkpICMgRGVmaW5pbmcgRnJlcXVlbmN5DQogICAgICAgIH0gZWxzZSB7DQogICAgICAgICAgZnJlcXMgPSByYmluZChmcmVxcyxjKDEsMCkpICMgRGVmaW5pbmcgRnJlcXVlbmN5IA0KICAgICAgICB9DQogICAgICAgIA0KICAgICAgICBkID0gZCsxDQogICAgICB9DQogICAgfQ0KICB9DQogIA0KICBzZXRUeHRQcm9ncmVzc0JhcihwYixpKQ0KfQ0KRGljdGlvbmFyeT1saXN0KCJXb3JkcyI9RGljdCwiRnJlcXVlbmNpZXMiPWZyZXFzWy0xLF0pDQogIHJldHVybihEaWN0aW9uYXJ5KQ0KfQ0KYGBgDQoNCg0KRmluYWxseSwgYSBmdW5jdGlvbiB0aGF0IHVzZXMgdGhlIGRpY3Rpb25hcnkgdG8gZXh0cmFjdCBmZWF0dXJlcy4NCmBgYHtyfQ0KRXh0cmFjdEZlYXR1cmVzIDwtIGZ1bmN0aW9uKGNvcnB1cywgZGljdCl7DQogIA0KICB0ZXggPSBjb3JwdXMkdGV4dA0KICBlbW9qaSA9IGNvcnB1cyRlbW9qaQ0KICANCiAgV29yZHM9ZGljdCRXb3Jkcw0KICBkaWN0JEZyZXF1ZW5jaWVzID0gZGljdCRGcmVxdWVuY2llcy9yb3dTdW1zKGRpY3QkRnJlcXVlbmNpZXMpDQogIEZyZXF1ZW5jaWVzPWRpY3QkRnJlcXVlbmNpZXMNCiAgDQogICNQcm9ncmVzcyBiYXINCiAgbiA9IGxlbmd0aCh0ZXgpDQogIHBiID0gdHh0UHJvZ3Jlc3NCYXIobWluID0gMCwgbWF4ID0gbiwgaW5pdGlhbCA9IDAsIHN0eWxlID0gMywNCndpZHRoID0gNTAsIGNoYXIgPSAiPSIpDQogIA0KICAgeCA9IG1hdHJpeChyZXAoMCxuKjMpLG5yb3cgPW4gLG5jb2w9MykNCiAgZm9yIChpIGluIDE6bikgew0KICAgIHdvcmRsaXN0ID0gbmFtZXMoc29ydCh0YWJsZSh1bmxpc3Qoc3Ryc3BsaXQodGV4W2ldLCAiICIpKSksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBkZWNyZWFzaW5nID0gVFJVRSkpDQogICAgaWYoaXMubnVsbChlbW9qaVtbaV1dKT09Ril7YXBwZW5kKHdvcmRsaXN0LHRhYmxlKGVtb2ppW2ldKSl9DQogICAgeFtpLDFdID0gMSAjYmlhcyB0ZXJtIGlzIHNldCB0byAxDQogICAgIyBsb29wIHRocm91Z2ggZWFjaCB3b3JkIGluIHRoZSBsaXN0IG9mIHdvcmRzDQogICAgZm9yICh3b3JkIGluIHdvcmRsaXN0KXsgDQogICAgICBpZiAoYW55KFdvcmRzPT13b3JkKSkgew0KICAgICAgICAjIGluY3JlbWVudCB0aGUgd29yZCBjb3VudCBmb3IgdGhlIG5ldXRyYWwgbGFiZWwgMA0KICAgICAgICB4W2ksMl0gPSB4W2ksMl0rRnJlcXVlbmNpZXNbd2hpY2goV29yZHM9PXdvcmQpLDFdDQogICAgICAgIyBpbmNyZW1lbnQgdGhlIHdvcmQgY291bnQgZm9yIHRoZSBwb3NpdGl2ZSBsYWJlbCAxDQogICAgICAgIHhbaSwzXSA9IHhbaSwzXStGcmVxdWVuY2llc1t3aGljaChXb3Jkcz09d29yZCksMl0NCiAgICAgIH0NCiAgICB9DQogICAgc2V0VHh0UHJvZ3Jlc3NCYXIocGIsaSkNCiAgfQ0KIHJldHVybih4KSANCn0NCmBgYA0KDQoNClVzaW5nIHRoZSBmdW5jdGlvbnMgZGVmaW5lZCwgd2UgYXJlIGdvaW5nIHRvIGNsZWFuIG91ciBQZXJzaWFuIHRleHQsIHRoZW4gYnVpbGQgYSBkaWN0aW9uYXJ5LCBhbmQgZXh0cmFjdCBmZWF0dXJlcyBmcm9tIHRoZSB0ZXh0Lg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRSwgcmVzdWx0cz1GQUxTRX0NClRleHRBZD1SZWZpbmVUZXh0KFRleHQpDQoNCkRpY3Rpb25hcnk9QnVpbGRGcmVxcyhsaXN0KCJ0ZXh0Ij1UZXh0QWQsImVtb2ppIj1FbW9qaSksWSkNCg0KWCA9IEV4dHJhY3RGZWF0dXJlcyhsaXN0KCJ0ZXh0Ij1UZXh0QWQsImVtb2ppIj1FbW9qaSksIERpY3Rpb25hcnkpDQoNCmBgYA0KDQoNCiMgQ2hlY2tpbmcgdGhlIGZlYXR1cmVzIGludGVycHJldGFiaWxpdHkNClRvIHNlZSwgd2VhdGhlciBpdCBpcyBwb3NzaWJsZSB0byB0byBjbGFzc2lmeSBvdXIgdGV4dCB1c2luZyB0aGUgZmVhdHVyZXMgd2UgYnVpbHQsIFdlIG11c3QgY2hlY2sgdGhlIGJlbG93IHNjYXR0ZXIgcGxvdC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpteWxhYmVsIDwtIGMoIlRyYWluIFBvcyIsICJUcmFpbiBOZWciKQ0KY29sb3JzIDwtIGMoImJsdWUiLCAicmVkIikNCg0KeGxhYmVsID0gIlN1bSBvZiBOZWdhdGl2ZSBXb3JkcyINCnlsYWJlbCA9ICJTdW0gb2YgUG9zaXRpdmUgV29yZHMiDQoNCnBsb3RfbHkoeD1YWywyXSx5PVhbLDNdLA0KICAgICAgICBzeW1ib2xzID0gYygneCcsJ28nKSwNCiAgICAgICAgbWFya2VyID0gbGlzdChzaXplID0gMTApLA0KICAgICAgICB0ZXh0PSBjKDE6bGVuZ3RoKFkpKSwgaG92ZXJpbmZvID0gInRleHQiLA0KICAgICAgICB0eXBlPSJzY2F0dGVyIixtb2RlPSJtYXJrZXJzIiwgY29sb3I9YXMuZmFjdG9yKFkpLGNvbG9ycyA9IGMoInJlZCIsICAiZ3JlZW4iKSkgJT4lDQogICAgICAgIGxheW91dCh0aXRsZSA9ICJDb21tZW50cyBieSB0aGUgRmVhdHVyZXMgYnVpbHQiLA0KICAgICAgICAgICAgICAgYXV0b3NpemUgPSBGLCB3aWR0aCA9IDkyMCwgaGVpZ2h0ID0gNTAwKQ0KYGBgDQoNCg0KDQpUaGUgZmlyc3QgY2xhc3NpZmljYXRpb24gbWV0aG9kIHRoYXQgaXMgd2lkZWx5IGJlaW5nIHVzZWQgYXMgYSB0cml2aWFsIGFuZCBlYXN5LXRvLWltcGxlbWVudCBtZXRob2RvbG9neSBpcyBOYWl2ZSBCYXllcy4gSW4gdGhlIGZvbGxvd2luZyBsaW5lcyBvZiBjb2RlLCB3ZSBhcmUgZ29pbmcgdG8gYnVpbGQgdHdvIGZ1bmN0aW9ucyBieSB3aGljaCB3ZSBjYW4gdXNlIHRoaXMgbWV0aG9kIGZvciBvdXIgZGF0YS4NCmBgYHtyfQ0KRXh0cmFjdFNlbnNlPWZ1bmN0aW9uKGNvcnB1cywgZGljdCl7DQogIA0KICB0ZXggPSBjb3JwdXMkdGV4dA0KICBlbW9qaSA9IGNvcnB1cyRlbW9qaQ0KICANCiAgV29yZHM9ZGljdCRXb3Jkcw0KICBGcmVxdWVuY2llcz1kaWN0JEZyZXF1ZW5jaWVzDQogIA0KICBkPWxlbmd0aChkaWN0JFdvcmRzKQ0KICBuPWxlbmd0aCh0ZXgpDQogIA0KICAjTGFwbGFjaWFuIFNtb290aGluZw0KICBGcmVxcz0xLyhjb2xTdW1zKEZyZXF1ZW5jaWVzKStkKSoodChGcmVxdWVuY2llcykrMSkNCiAgRnJlcXM9dChGcmVxcykNCiAgDQogIHBfd19wb3M9RnJlcXNbLDJdDQogIHBfd19uZWc9RnJlcXNbLDFdDQogIA0KICBsb2dsaWtlbGlob29kID0gbG9nKHBfd19wb3MvcF93X25lZykNCiAgDQogIHA9cmVwKDAsbikNCiAgDQogIHBiID0gdHh0UHJvZ3Jlc3NCYXIobWluID0gMCwgbWF4ID0gbiwgaW5pdGlhbCA9IDAsIHN0eWxlID0gMywNCndpZHRoID0gNTAsIGNoYXIgPSAiPSIpDQogIA0KICBmb3IoaSBpbiAxOm4pew0KICAgIA0KICAgIHdvcmRsaXN0ID0gc29ydCh0YWJsZSh1bmxpc3Qoc3Ryc3BsaXQodGV4W2ldLCAiICIpKSksIGRlY3JlYXNpbmcgPSBUUlVFKQ0KICAgIGlmKGlzLm51bGwoZW1vamlbW2ldXSk9PUYpe2FwcGVuZCh3b3JkbGlzdCx0YWJsZShlbW9qaVtpXSkpfQ0KICAgIA0KICAgIGZvciAoaiBpbiAxOmxlbmd0aCh3b3JkbGlzdCkpew0KICAgICAgaWYgIChhbnkoV29yZHMgPT0gbmFtZXMod29yZGxpc3QpW2pdKSkgew0KICAgICAgICBwW2ldPXBbaV0rd29yZGxpc3Rbal0qbG9nbGlrZWxpaG9vZFtXb3JkcyA9PSBuYW1lcyh3b3JkbGlzdClbal1dDQogICAgICB9DQogICAgfQ0KICAgIHNldFR4dFByb2dyZXNzQmFyKHBiLGkpDQogIH0NCiAgcmV0dXJuKHApDQp9DQpgYGANCg0KDQpBbmQsIHRoZSBwcmVkaWN0aW9yIGZ1bmN0aW9uIHdpbGwgYmUgZGVmaW5lZCBsaWtlIHRoaXM6DQpgYGB7ciB3YXJuaW5nPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0KTmFpdmVCYXllc1ByZWRpY3RvcjwtZnVuY3Rpb24ocCx5KXsNCiAgDQogIERfcG9zPXN1bSh5PT0xKQ0KICBEX25lZz1zdW0oeT09MCkNCiAgDQogIGxvZ3ByaW9yID0gbG9nKERfcG9zL0RfbmVnKQ0KICANCiAgcD1wK3JlcChsb2dwcmlvcixsZW5ndGgocCkpDQogIA0KICBpZiAobGVuZ3RoKHApPT1sZW5ndGgoeSkpew0KICAgIHloYXQgPSAocD4wKQ0KICAgIGFjY3VyYWN5ID0gbWVhbih5PT15aGF0KQ0KICB9DQogIA0KICByZXR1cm4obGlzdCgiWWhhdCI9eWhhdCwiQWNjdXJhY3kiPWFjY3VyYWN5KSkNCn0NCmBgYA0KDQoNCk5vdywgd2Ugd2FudCB0byB1c2UgdGhpcyBtZXRob2Qgb24gb3VyIGRhdGEgdG8gYnVpbGQgYSBmZWF0dXJlIGNhbGxlZCAiU2Vuc2UiIHRoYXQgbWVhc3VyZXMgdGhlIGludGVuc2l0eSBvZiBlYWNoIGNvbW1lbnQgYWxvbmdzaWRlIHRoZWlyIHBvbGFyaXR5Lg0KYGBge3IgZWNobz1UUlVFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCByZXN1bHRzPUZBTFNFfQ0KU2Vuc2U9RXh0cmFjdFNlbnNlKGxpc3QoInRleHQiPVRleHRBZCwiZW1vamkiPUVtb2ppKSwgRGljdGlvbmFyeSkNCmBgYA0KDQoNCldlIGNhbiBzZWUgYmVsb3cgaG93IHRoaXMgZmVhdHVyZSBsb29rcyBsaWtlLg0KYGBge3Igd2FybmluZz1GQUxTRX0NCnBsb3RfbHkoeD0xOmxlbmd0aChTZW5zZSkseT1TZW5zZSwgdHlwZSA9ICdiYXInKSAlPiUNCiAgbGF5b3V0KHRpdGxlID0gIk92ZXJhbCBQb2xhcml0eSBvZiBBbGwgdGhlIENvbW1lbnRzIiwNCiAgICAgICAgIHBsb3RfYmdjb2xvcj0nI2U1ZWNmNicsYXV0b3NpemUgPSBULCB3aWR0aCA9IDg4MCwgaGVpZ2h0ID0gNTAwKQ0KYGBgDQoNCg0KVGhlIGFjY3VyYWN5IG9mIE5haXZlIEJheWVzIGNsYXNzaWZpZXIgb24gdGhlIHRyYWluIGRhdGFzZXQgY2FuIGJlIGNhbGN1bGF0ZWQgYXMgZm9sbG93Lg0KYGBge3J9DQoNCk5CPU5haXZlQmF5ZXNQcmVkaWN0b3IoU2Vuc2UsWSkNCnloYXQ9TkIkWWhhdA0KDQoNCk5CQWNjdXJhY3k9YygpDQpOQkFjY3VyYWN5WyJQb3NpdGl2ZSJdID0gDQogIG1lYW4obmEub21pdChZW1k9PTFdPT15aGF0W1k9PTFdKSkNCk5CQWNjdXJhY3lbIk5lZ2F0aXZlIl0gPSANCiAgbWVhbihuYS5vbWl0KFlbWT09MF09PXloYXRbWT09MF0pKQ0KDQpjYXQocGFzdGUwKCJPdmVyYWwgQWNjdXJhY3k6IFxuIiksbWVhbih5aGF0PT1ZKSwNCiAgICBwYXN0ZTAoIlxuQWNjdXJhY3kgZm9yIGRpZmZlcmVudCBzZW50aW1lbnRzOiBcbiIpLCBuYW1lcyhOQkFjY3VyYWN5KSwNCiAgICBwYXN0ZTAoIiBcbiIpLE5CQWNjdXJhY3kpDQpgYGANCg0KDQpUaGUgc2Vjb25kIGNsYXNzaWZpZXIgaXMgYmFzZWQgb24gdmVjdG9yIHNwYWNlIG1vZGVscy4gV2UgYXJlIGdvaW5nIHRvIGJ1aWxkIHN1Y2ggYSBjbGFzc2lmaWVyIGluIHRoZSBmb2xsb3dpbmcgbGluZXMgb2YgY29kZS4NCmBgYHtyfQ0KVmVjdG9yU3BhY2VNb2RlbD0gZnVuY3Rpb24oeCx5KXsNCiAgDQogIHBvc19jZW50ZXI9Y29sTWVhbnMoeFt5PT0xLF0pDQogIG5lZ19jZW50ZXI9Y29sTWVhbnMoeFt5PT0wLF0pDQogIGNlbnRlcnM9Y2JpbmQocG9zX2NlbnRlclstMV0sbmVnX2NlbnRlclstMV0pDQoNCiAgZG90X3Byb2RzPXhbLC0xXSUqJWNlbnRlcnMNCiAgbm9ybVg9YXBwbHkoeFssLTFdLDEsZnVuY3Rpb24oeCkoc3FydChzdW0oeF4yKSkpKQ0KICBub3JtQ2VudGVyPWFwcGx5KGNlbnRlcnMsMixmdW5jdGlvbih4KShzcXJ0KHN1bSh4XjIpKSkpDQogIA0KICBhbmdlbF9jb3NpbmU9YXMubWF0cml4KDEvbm9ybVgpJSoldChhcy5tYXRyaXgoMS9ub3JtQ2VudGVyKSkqKGRvdF9wcm9kcykNCiAgDQogIHloYXQ9YXBwbHkoYW5nZWxfY29zaW5lLCAxLCBmdW5jdGlvbih4KSB3aGljaChtYXgoeCk9PXgpKQ0KICB5aGF0W3loYXQ9PTJdPTANCiAgcmV0dXJuKGxpc3QoIlloYXQiPXloYXQsIkNlbnRlcnMiPWNlbnRlcnMpKQ0KfQ0KYGBgDQoNCg0KVGhlIG5leHQgZnVuY3Rpb24gaXMgZ29pbmcgdG8gY2FsY3VsYXRlcyAocHJlZGljdHMpIHRoZSBwb2xhcml0eSBmb3IgZWFjaCBjb21tZW50LiBQbHVzLCBpZiB3ZSBpbnB1dCB0aGUgZGVwZW5kZW50IHZhcmlhYmxlICh5KSBhcyB3ZWxsIGl0IGdpdmVzIHVzIHRoZSBhY2N1cmFjeSBvZiB0aGUgcHJlZGljdGlvbnMuDQpgYGB7cn0NClZlY3RvclNwYWNlUHJlZGljdG9yPSBmdW5jdGlvbih4LHk9TkEsY2VudGVycyl7DQoNCiAgZG90X3Byb2RzPXhbLC0xXSUqJWNlbnRlcnMNCiAgbm9ybVg9YXBwbHkoeFssLTFdLDEsZnVuY3Rpb24oeCkoc3FydChzdW0oeF4yKSkpKQ0KICBub3JtQ2VudGVyPWFwcGx5KGNlbnRlcnMsMixmdW5jdGlvbih4KShzcXJ0KHN1bSh4XjIpKSkpDQogIA0KICBhbmdlbF9jb3NpbmU9YXMubWF0cml4KDEvbm9ybVgpJSoldChhcy5tYXRyaXgoMS9ub3JtQ2VudGVyKSkqKGRvdF9wcm9kcykNCiAgDQogIHloYXQ9YXBwbHkoYW5nZWxfY29zaW5lLCAxLCBmdW5jdGlvbih4KSB3aGljaChtYXgoeCk9PXgpKQ0KICB5aGF0W3loYXQ9PTJdPTANCiAgDQogIGFjY3VyYWN5PSBOQQ0KICANCiAgaWYoaXMubmEoeSk9PUZBTFNFICYmIGxlbmd0aCh5KT09bGVuZ3RoKHloYXQpKXsNCiAgICBhY2N1cmFjeSA9IG1lYW4oeT09eWhhdCkNCiAgfQ0KICByZXR1cm4obGlzdCgiWWhhdCI9eWhhdCwiQWNjdXJhY3kiPWFjY3VyYWN5KSkNCn0NCmBgYA0KDQoNClRyYWluIGFjY3VyYWN5IGZvciBWZWN0b3IgU3BhY2UgY2xhc3NpZmllciBpcyBhcyBmb2xsb3dzLg0KYGBge3Igd2FybmluZz1GQUxTRX0NCg0KZml0VlM9VmVjdG9yU3BhY2VNb2RlbChYLFkpDQp5aGF0PWZpdFZTJFloYXQNCg0KDQpWU0FjY3VyYWN5PWMoKQ0KVlNBY2N1cmFjeVsiUG9zaXRpdmUiXSA9IA0KICBtZWFuKG5hLm9taXQoWVtZPT0xXT09eWhhdFtZPT0xXSkpDQpWU0FjY3VyYWN5WyJOZWdhdGl2ZSJdID0gDQogIG1lYW4obmEub21pdChZW1k9PTBdPT15aGF0W1k9PTBdKSkNCg0KY2F0KHBhc3RlMCgiT3ZlcmFsIEFjY3VyYWN5OiBcbiIpLG1lYW4oeWhhdD09WSksDQogICAgcGFzdGUwKCJcbkFjY3VyYWN5IGZvciBkaWZmZXJlbnQgc2VudGltZW50czogXG4iKSwgbmFtZXMoVlNBY2N1cmFjeSksDQogICAgcGFzdGUwKCIgXG4iKSxWU0FjY3VyYWN5KQ0KYGBgDQoNCg0KVGhlIGxvZ2lzdGljIHJlZ3Jlc3Npb24gY2xhc3NpZmllciBhY2N1cmFjeSBvbiB0aGlzIGRhdGFzZXQgY2FuIGJlIGNhbGN1bGF0ZWQgYXMgZm9sbG93Lg0KYGBge3Igd2FybmluZz1GQUxTRX0NCmZpdEdMTT1nbG0oWX5YWywtMV0sZmFtaWx5PSJiaW5vbWlhbCIpDQp5aGF0PXJvdW5kKHByZWRpY3QoZml0R0xNLGFzLmRhdGEuZnJhbWUoWCksdHlwZT0icmVzcG9uc2UiKSwwKQ0KDQpHTE1BY2N1cmFjeT1jKCkNCkdMTUFjY3VyYWN5WyJQb3NpdGl2ZSJdID0gDQogIG1lYW4obmEub21pdChZW1k9PTFdPT15aGF0W1k9PTFdKSkNCkdMTUFjY3VyYWN5WyJOZWdhdGl2ZSJdID0gDQogIG1lYW4obmEub21pdChZW1k9PTBdPT15aGF0W1k9PTBdKSkNCg0KY2F0KHBhc3RlMCgiT3ZlcmFsIEFjY3VyYWN5OiBcbiIpLG1lYW4oeWhhdD09WSksDQogICAgcGFzdGUwKCJcbkFjY3VyYWN5IGZvciBkaWZmZXJlbnQgc2VudGltZW50czogXG4iKSwgbmFtZXMoR0xNQWNjdXJhY3kpLA0KICAgIHBhc3RlMCgiIFxuIiksR0xNQWNjdXJhY3kpDQoNCmBgYA0KSXQgaXMgbm90IG9idmlvdXMgd2hpY2ggbWV0aG9kIHBlcmZvcm1zIGJldHRlciBvbiB0aGlzIGRhdGFzZXQuIFNvLCB3ZSBydW4gYSBzaW11bGF0aW9uIG9mIDEwMCBpdGVyYXRpb25zIGVhY2ggdGltZSB3ZSBhcmUgZ29pbmcgdG8gcmFuZG9tbHkgc3BsaXQgdGhlIGRhdGFzZXQgaW50byB0cmFpbiBhbmQgdGVzdC4gVGhlbiB3ZSBidWlsZCBvdXIgbW9kZWxzIGJhc2VkIG9uIHRoZSB0cmFpbiBzZXRzIGFuZCB0ZXN0IHRoZWlyIHBlcmZvcm1hbmNlcyBvbiB0aGUgdGVzdCBzZXRzLiB0aGUgcmVzdWx0cyB3ZSBiZSBzdG9yZWQgaW4gYSBtYXRyaXggYW5kIGFmdGVyIHRoZSBzaW11bGF0aW9uIHdpbGwgYmUgdmlzdWFsaXplZC4NCg0KDQpTbywgdGhlIHNpbXVsYXRpb24gZm9yIHRoZSBkZWZpbmVkIG1ldGhvZG9sb2dpZXMgaXMgZ29pbmcgdG8gYmUgbGlrZSB0aGlzLg0KYGBge3Igd2FybmluZz1GQUxTRSwgaW5jbHVkZT1GQUxTRX0NClBvc1ggPSBYW1k9PTEsXQ0KTmVnWCA9IFhbWT09MCxdDQoNClBvc1NlbnNlID0gU2Vuc2VbWT09MV0NCk5lZ1NlbnNlID0gU2Vuc2VbWT09MF0NCg0KblA9ZGltKFBvc1gpWzFdDQpuTj1kaW0oTmVnWClbMV0NCkFjY3VyYWNpZXM9MA0KDQpFcnJvcj1tYXRyaXgocmVwKDAsMyoxMDApLG5jb2w9MykNCg0KZm9yKGl0ZXIgaW4gMToxMDApew0KICBpbmRleFAgPSBzYW1wbGUoblAsY2VpbGluZyhuUCowLjkpLHJlcGxhY2UgPSBGKQ0KICBpbmRleE4gPSBzYW1wbGUobk4sY2VpbGluZyhuTiowLjkpLHJlcGxhY2UgPSBGKQ0KDQogIFRyYWluWFBvcyA9IFBvc1hbaW5kZXhQLF0NCiAgVHJhaW5YTmVnID0gTmVnWFtpbmRleE4sXQ0KDQogIFRlc3RYUG9zID0gUG9zWFstaW5kZXhQLF0NCiAgVGVzdFhOZWcgPSBOZWdYWy1pbmRleE4sXQ0KICANCiAgVGVzdFNlbnNlUG9zID0gYXMubWF0cml4KFBvc1NlbnNlWy1pbmRleFBdKQ0KICBUZXN0U2Vuc2VOZWcgPSBhcy5tYXRyaXgoTmVnU2Vuc2VbLWluZGV4Tl0pDQogIA0KICBTZW5zZVRlc3QgPSBhcHBlbmQoVGVzdFNlbnNlUG9zLFRlc3RTZW5zZU5lZykNCiAgDQogIFhUcmFpbiA9IHJiaW5kKFRyYWluWFBvcyxUcmFpblhOZWcpDQogIFhUZXN0ID0gcmJpbmQoVGVzdFhQb3MsVGVzdFhOZWcpDQoNCiAgVHJhaW5ZID0gYyhyZXAoMSxkaW0oVHJhaW5YUG9zKVsxXSkscmVwKDAsZGltKFRyYWluWE5lZylbMV0pKQ0KICBUZXN0WSA9IGMocmVwKDEsZGltKFRlc3RYUG9zKVsxXSkscmVwKDAsZGltKFRlc3RYTmVnKVsxXSkpDQoNCiAgZml0VlM9VmVjdG9yU3BhY2VNb2RlbChYVHJhaW4sVHJhaW5ZKQ0KICBWUz1WZWN0b3JTcGFjZVByZWRpY3RvcihYVGVzdCxUZXN0WSxmaXRWUyRDZW50ZXJzKQ0KICANCiAgTkI9TmFpdmVCYXllc1ByZWRpY3RvcihTZW5zZVRlc3QsVGVzdFkpDQogIA0KICBmaXRHTE09Z2xtKFRyYWluWX5YVHJhaW5bLC0xXSxmYW1pbHk9ImJpbm9taWFsIikNCiAgbGFiZWxHTE09cHJlZGljdChmaXRHTE0sYXMuZGF0YS5mcmFtZShYVGVzdCksdHlwZT0icmVzcG9uc2UiKQ0KICANCiAgRXJyb3JbaXRlcixdPWMoMS1WUyRBY2N1cmFjeSwxLU5CJEFjY3VyYWN5LDEtKG1lYW4ocm91bmQobGFiZWxHTE0sMCk9PVRlc3RZKSkpDQp9DQpgYGANCg0KDQpTbywgaW4gdGhlIGZvbGxvd2luZyBsaW5lcyBvZiBjb2RlLCB3ZSBhcmUgZ29pbmcgdG8gYnVpbGQgYSBib3ggcGxvdCB0byBjb21wYXJlIHRoZSBlcnJvciBvZiBlYWNoIG1ldGhvZC4NCmBgYHtyIHdhcm5pbmc9RkFMU0V9DQpmaWcgPC0gcGxvdF9seSh0eXBlID0gJ2JveCcpDQpmaWcgPC0gZmlnICU+JSBhZGRfYm94cGxvdCh5ID0gRXJyb3JbLDFdLCBqaXR0ZXIgPSAwLjMsIHBvaW50cG9zID0gLTEuOCwgYm94cG9pbnRzID0gJ2FsbCcsDQogICAgICAgICAgICAgIG1hcmtlciA9IGxpc3QoY29sb3IgPSAncmdiKDcsNDAsODkpJyksDQogICAgICAgICAgICAgIGxpbmUgPSBsaXN0KGNvbG9yID0gJ3JnYig3LDQwLDg5KScpLA0KICAgICAgICAgICAgICBuYW1lID0gIlZlY3RvciBTcGNhZSBDbGFzc2lmaWVyIikNCmZpZyA8LSBmaWcgJT4lIGFkZF9ib3hwbG90KHkgPSBFcnJvclssMl0sIGppdHRlciA9IDAuMywgcG9pbnRwb3MgPSAtMS44LCBib3hwb2ludHMgPSAnYWxsJywNCiAgICAgICAgICAgICAgbWFya2VyID0gbGlzdChjb2xvciA9ICdyZ2IoOSw1NiwxMjUpJyksDQogICAgICAgICAgICAgIGxpbmUgPSBsaXN0KGNvbG9yID0gJ3JnYig5LDU2LDEyNSknKSwNCiAgICAgICAgICAgICAgbmFtZSA9ICJOYWl2ZSBCYXllcyBDbGFzc2lmaWVyIikNCmZpZyA8LSBmaWcgJT4lIGFkZF9ib3hwbG90KHkgPSBFcnJvclssM10sIGppdHRlciA9IDAuMywgcG9pbnRwb3MgPSAtMS44LCBib3hwb2ludHMgPSAnYWxsJywNCiAgICAgICAgICAgICAgbWFya2VyID0gbGlzdChjb2xvciA9ICdyZ2IoMTA3LDE3NCwyMTQpJyksDQogICAgICAgICAgICAgIGxpbmUgPSBsaXN0KGNvbG9yID0gJ3JnYigxMDcsMTc0LDIxNCknKSwNCiAgICAgICAgICAgICAgbmFtZSA9ICJMb2dpc3RpYyBSZWdyZXNzaW9uIENsYXNzaWZpZXIiKQ0KDQpmaWcgJT4lIGxheW91dChhdXRvc2l6ZSA9IEYsIHdpZHRoID0gOTIwLCBoZWlnaHQgPSA1MDApDQpgYGANCkl0IHNlZW1zIG9idmlvdXMgbm93IHRoYXQgdmVjdG9yIHNwYWNlIGNsYXNzaWZpZXIgcGVyZm9ybXMgc2xpZ2h0bHkgYmV0dGVyIHRoYW4gbmFpdmUgQmF5ZXMgYW5kIHNpZ25pZmljYW50bHkgYmV0dGVyIHRoYW4gTG9naXN0aWMgcmVncmVzc2lvbiBjbGFzc2lmaWVyLg0KDQojIFZpc3VhbGl6YXRpb25zIGhlbHBmdWwgZm9yIGEgc2VudGltZW50IGFuYWx5c2lzIHJlcG9ydC4NCg0KVGhlIGRvdWdobnV0IGNoYXJ0IGZvciBzZW50aW1lbnQgYnJlYWtkb3duIG9mIHRoZSBjb3JwdXMuDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KVmFsdWVzIDwtIGMobGVuZ3RoKFlbWT09MV0pLGxlbmd0aChZW1k9PTBdKSkNCmxhYmVscyA8LSBjKCJQb3NpdGl2ZSIsICJOYWdhdGl2ZSIgKQ0KY29sb3JzIDwtIGMoImdyZWVuIiwgInJlZCIpDQoNCmZpZzEgPC0gcGxvdF9seShhcy5kYXRhLmZyYW1lKFZhbHVlcyksIGxhYmVscyA9IGxhYmVscywgdmFsdWVzID0gVmFsdWVzKSAlPiUNCiAgYWRkX3BpZShob2xlID0gMC42LCBtYXJrZXI9bGlzdChjb2xvcnM9Y29sb3JzKSkgJT4lDQogIGxheW91dCh0aXRsZSA9ICJTZW50aW1lbnQgQnJlYWtkb3duIiwgIHNob3dsZWdlbmQgPSBGLCANCiAgICAgICAgIHhheGlzID0gbGlzdChzaG93Z3JpZCA9IEZBTFNFLCB6ZXJvbGluZSA9IEZBTFNFLCBzaG93dGlja2xhYmVscyA9IFRSVUUpLA0KICAgICAgICAgeWF4aXMgPSBsaXN0KHNob3dncmlkID0gRkFMU0UsIHplcm9saW5lID0gRkFMU0UsIHNob3d0aWNrbGFiZWxzID0gVFJVRSkpDQpmaWcxICU+JSBsYXlvdXQoYXV0b3NpemUgPSBGLCB3aWR0aCA9IDgwMCwgaGVpZ2h0ID0gNTAwKQ0KYGBgDQoNCg0KDQpUaGUgR2F1Z2UgY2hhcnQgZm9yIG92ZXJhbGwgc2VudGltZW50IHNjb3JlIG9mIHRoZSBjb3JwdXMuDQpgYGB7ciBlY2hvPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kc2NvcmU9cm91bmQobWVhbihZKSwyKQ0KZmlnMiA8LSBwbG90X2x5KA0KICBkb21haW4gPSBsaXN0KHggPSBjKDAsIDEpLCB5ID0gYygwLCAxKSksDQogIHZhbHVlID0gc2NvcmUsDQogIHRpdGxlID0gbGlzdCh0ZXh0ID0gIlNwZWVkIiksDQogIHR5cGUgPSAiaW5kaWNhdG9yIiwNCiAgbW9kZSA9ICJnYXVnZStudW1iZXIiLA0KICBkZWx0YSA9IGxpc3QocmVmZXJlbmNlID0gc2NvcmUpLA0KICBnYXVnZSA9IGxpc3QoDQogICAgYXhpcyA9bGlzdChyYW5nZSA9IGxpc3QoLTEsIDEpKSkpIA0KZmlnMiA8LSBmaWcyICU+JQ0KICBsYXlvdXQobWFyZ2luID0gbGlzdChsPTIwLHI9MzApKQ0KZmlnMiAlPiUgbGF5b3V0KGF1dG9zaXplID0gRiwgd2lkdGggPSA5MjAsIGhlaWdodCA9IDQwMCkNCmBgYA0KDQoNCg0KRmluYWxseSwgYSB3b3JkY2xvdWQgY2FuIGJlYXV0aWZ5IHlvdXIgcmVwb3J0Lg0KYGBge3IgZWNobz1GQUxTRSwgZmlnLmhlaWdodD04LCBmaWcud2lkdGg9OSwgd2FybmluZz1GQUxTRX0NCmY9YygpDQpmJFdvcmRzPURpY3Rpb25hcnkkV29yZHMNCmYkRnJlcXVlbmNpZXM9cm93U3VtcyhEaWN0aW9uYXJ5JEZyZXF1ZW5jaWVzKQ0KDQp3b3JkY2xvdWQyKGRhdGE9Ziwgc2l6ZT0xLjYpIA0KYGBgDQoNCg0KDQoNCg0K